package com.nulldev.util.networking.ip;

import java.io.IOException;
import java.net.InetAddress;
import java.net.NetworkInterface;
import java.net.ServerSocket;
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.Enumeration;
import java.util.List;
import java.util.regex.Pattern;

import com.nulldev.util.JVM.Arguments;
import com.nulldev.util.JVM.JVM;
import com.nulldev.util.VariableAPI.ArrayUtils;
import com.nulldev.util.VariableAPI.RandomUtil;
import com.nulldev.util.data.Variables;
import com.nulldev.util.internal.backport.arrays.BackportList;
import com.nulldev.util.logging.Logger;
import com.nulldev.util.logging.LoggerFactory;
import com.nulldev.util.manage.nullUtil;
import com.nulldev.util.osAPI.OS;
import com.nulldev.util.osAPI.Packages;

public class IP {
	private static final Logger log = LoggerFactory.getLoggerD(Arguments.hasArgument("--IP.enableDebug"));
	private static final boolean PREFER_IPV6_ADDRESSES = Arguments.hasArgument("--IP.preferV6");

	public static String generateRandomIP() {
		return RandomUtil.randInt(0, 255) + "." + RandomUtil.randInt(0, 255) + "." + RandomUtil.randInt(0, 255) + "." + RandomUtil.randInt(0, 255);
	}

	public static InetAddress localhost() throws UnknownHostException {
		try {
			final InetAddress local = InetAddress.getLocalHost();
			final String lx = local.getHostAddress();
			if (!lx.startsWith("127.") && !lx.startsWith("0.")) {
				return InetAddress.getByName("127.0.0.1");
			}
			return InetAddress.getByName(lx);
		} catch (UnknownHostException f) {
			return InetAddress.getByName("0.0.0.0");
		}
	}

	public static final String[] BLACKLISTED_IP_INTERFACES = new String[]
		{
				/* Linux/BSD specific */
				"virbr", "anbox", "pan", "lxdbr", "zt", "ap", "waydroid", "veth", "docker" };

	/**
	 * Interface Provided By virbr libvirt (Linux) anbox Anbox (Linux) pan ???
	 * (Linux) lxdbr lxc (Linux) zt ZeroTier (Linux) ap Access Points (Linux)
	 * waydroid Waydroid (Linux) veth ???????? (Linux) docker Docker (Linux)
	 */
	private static boolean enableIPInterfaceBlacklist = (Packages.isPackage("libvirtd") || Packages.isPackage("anbox") || Packages.isPackage("zerotier-cli")
			|| Packages.isPackage("create_ap") || Packages.isPackage("docker")) && Arguments.notArguments("--IPUtil.disableIPIntBlacklist");

	public static boolean isIPInterfaceBlacklistActive() {
		return enableIPInterfaceBlacklist;
	}

	public static void setIPInterfaceBlacklistEnabled(final boolean value) {
		enableIPInterfaceBlacklist = value;
	}

	public static InetAddress lanAddress() throws UnknownHostException {
		try {
			InetAddress candidateAddress = null;
			// Iterate all NICs (network interface cards)...
			for (final Enumeration<NetworkInterface> ifaces = NetworkInterface.getNetworkInterfaces(); ifaces.hasMoreElements();) {
				final NetworkInterface iface = (NetworkInterface) ifaces.nextElement();
				if (log.isDebugEnabled()) {
					log.debug("Interface: " + iface.getName() + " (" + iface.getDisplayName() + ")");
					log.debug("IPs: ");
					InetAddress ina;
					final Enumeration<InetAddress> addrs = iface.getInetAddresses();
					while (addrs.hasMoreElements() && (ina = addrs.nextElement()) != null) {
						String trn = ina.getHostAddress();
						if (trn.contains("%"))
							trn = trn.substring(0, trn.indexOf("%"));
						log.debug("\t" + ina + " (ipv6: " + isIPv6Address(trn) + ")");
					}

				}
				if (enableIPInterfaceBlacklist) {
					if (OS.isLinux() && ArrayUtils.StringArrayUtils.containsStartsWithInverse(BLACKLISTED_IP_INTERFACES, iface.getName())) {
						continue;
					}
				}
				// JVMUtil.async_println(iface.getDisplayName() + ",name=" + iface.getName() +
				// ",ip=" + iface.getInterfaceAddresses().get(0).getAddress());
				// Iterate all IP addresses assigned to each card...
				final List<InetAddress> addresses = new ArrayList<InetAddress>();
				final Enumeration<InetAddress> na = iface.getInetAddresses();
				while (na.hasMoreElements())
					addresses.add(na.nextElement());
				BackportList.sort(addresses, new Comparator<InetAddress>() {

					@Override
					public int compare(final InetAddress o1, final InetAddress o2) {
						String trn1 = o1.getHostAddress();
						if (trn1.contains("%"))
							trn1 = trn1.substring(0, trn1.indexOf("%"));
						String trn2 = o2.getHostAddress();
						if (trn2.contains("%"))
							trn2 = trn2.substring(0, trn2.indexOf("%"));
						if (isIPv6Address(trn2) && !PREFER_IPV6_ADDRESSES)
							return -1;
						if (isIPv4Address(trn1) && !PREFER_IPV6_ADDRESSES)
							return 1;
						return 0;
					}

				});
				for (final InetAddress inetAddr : addresses) {
					if (!inetAddr.isLoopbackAddress()) {
						if (inetAddr.isSiteLocalAddress()) {
							// Found non-loopback site-local address. Return it immediately...
							return inetAddr;
						} else if (candidateAddress == null) {
							// Found non-loopback address, but not necessarily site-local.
							// Store it as a candidate to be returned if site-local address is not
							// subsequently found...
							candidateAddress = inetAddr;
							// Note that we don't repeatedly assign non-loopback non-site-local addresses as
							// candidates,
							// only the first. For subsequent iterations, candidate will be non-null.
						}
					}
				}
			}
			if (candidateAddress != null) {
				// We did not find a site-local address, but we found some other non-loopback
				// address.
				// Server might have a non-site-local address assigned to its NIC (or it might
				// be running
				// IPv6 which deprecates the "site-local" concept).
				// Return this non-loopback candidate address...
				return candidateAddress;
			}
			// At this point, we did not find a non-loopback address.
			// Fall back to returning whatever InetAddress.getLocalHost() returns...
			final InetAddress jdkSuppliedAddress = InetAddress.getLocalHost();
			if (jdkSuppliedAddress == null) {
				throw new UnknownHostException("The JDK InetAddress.getLocalHost() method unexpectedly returned null.");
			}
			return jdkSuppliedAddress;
		} catch (Exception e) {
			final UnknownHostException unknownHostException = new UnknownHostException("Failed to determine LAN address: " + e);
			unknownHostException.initCause(e);
			throw unknownHostException;
		}
	}

	public static boolean isLocalPortFree(int port) {
		try {
			new ServerSocket(port).close();
			return true;
		} catch (IOException e) {
			return false;
		}
	}

	private static int prevPort;

	public static boolean isPortFree(int port) {
		try {
			new ServerSocket(port, 0, lanAddress()).close();
			return true;
		} catch (IOException e) {
			return false;
		}
	}

	public static int nextFreePort(int from, int to) {
		int port = RandomUtil.randInt(from, to);
		while (true) {
			if (isLocalPortFree(port)) {
				return prevPort = port;
			} else {
				port = RandomUtil.randInt(from, to);
			}
		}
	}

	public static int freePort(final int from, final int to) {
		for (int i = from; i < to; i++) {
			if (isLocalPortFree(i)) {
				return prevPort = i;
			}
		}
		return nextFreePort(from, to);
	}

	public static int prevGeneratedPort() {
		return prevPort;
	}

	public static boolean supportsSCTP() {
		if (nullUtil.IS_ANDROID_BUILD) {
			return false;
		} else {
			try {
				com.sun.nio.sctp.SctpChannel.open().close();
				return true;
			} catch (Exception e) {
				return false;
			}
		}
	}

	private static final Pattern IPV4_PATTERN = Pattern.compile("^(25[0-5]|2[0-4]\\d|[0-1]?\\d?\\d)(\\.(25[0-5]|2[0-4]\\d|[0-1]?\\d?\\d)){3}$");

	private static final Pattern IPV6_STD_PATTERN = Pattern.compile("^(?:[0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}$");

	private static final Pattern IPV6_HEX_COMPRESSED_PATTERN = Pattern
			.compile("^((?:[0-9A-Fa-f]{1,4}(?::[0-9A-Fa-f]{1,4})*)?)::((?:[0-9A-Fa-f]{1,4}(?::[0-9A-Fa-f]{1,4})*)?)$");

	public static boolean isIPv4Address(final String input) {
		return IPV4_PATTERN.matcher(input).matches();
	}

	public static boolean isIPv6StdAddress(final String input) {
		return IPV6_STD_PATTERN.matcher(input).matches();
	}

	public static boolean isIPv6HexCompressedAddress(final String input) {
		return IPV6_HEX_COMPRESSED_PATTERN.matcher(input).matches();
	}

	public static boolean isIPv6Address(final String input) {
		return isIPv6StdAddress(input) || isIPv6HexCompressedAddress(input);
	}

	public static boolean isIP(final String ip) {
		return isIPv4Address(ip) || isIPv6Address(ip);
	}

	public static void main(String[] args) throws Exception {
		JVM.println("LAN IP: " + lanAddress());
	}

	public static String publicIP() throws UnknownHostException {
		// TODO: Placeholder
		return lanAddress().toString();
	}

	public static InetAddress allAddresses() throws UnknownHostException {
		if (OS.isLinux() || OS.isBSD() || OS.isMacOS() || OS.isWindows()) {
			return InetAddress.getByName("::");
		} else {
			return InetAddress.getByName("127.0.0.1");
		}
	}

	public static InetAddress convertToIP(final String _ip) throws UnknownHostException {
		return InetAddress.getByName(Variables.requireNonNullObject(_ip, "_ip"));
	}
}
